package jst.btremote.bluetooth;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.UUID;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;

public class BluetoothConnector {

	private BluetoothSocketWrapper bluetoothSocket;
	private BluetoothDevice device;
	private BluetoothAdapter adapter;
	private boolean connectionStatus;

	/**
	 * @param device
	 *            the device
	 * @param secure
	 *            if connection should be done via a secure socket
	 * @param adapter
	 *            the Android BT adapter
	 * @param uuidCandidates
	 *            a list of UUIDs. if null or empty, the Serial PP id is used
	 */
	public BluetoothConnector(BluetoothDevice device, BluetoothAdapter adapter) {
		this.device = device;
		this.adapter = adapter;
		this.bluetoothSocket = null;
		this.connectionStatus = false;
	}
	
	public BluetoothSocketWrapper connect() throws IOException {
		this.connectionStatus = false;
		
		bluetoothSocket = new NativeBluetoothSocket(device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")));
		adapter.cancelDiscovery();

		try {
			bluetoothSocket.connect();
			this.connectionStatus = true;
		} catch (IOException e) {
			// try the fallback
			try {
				bluetoothSocket = new FallbackBluetoothSocket(bluetoothSocket.getUnderlyingSocket());
				Thread.sleep(500);
				bluetoothSocket.connect();
				this.connectionStatus = true;
			} catch (FallbackException e1) {
				Log.w("BT", "Could not initialize FallbackBluetoothSocket classes.", e);
			} catch (InterruptedException e1) {
				Log.w("BT", e1.getMessage(), e1);
			} catch (IOException e1) {
				Log.w("BT", "Fallback failed. Cancelling.", e1);
			}
		}

		if (!this.connectionStatus) {
			throw new IOException("Could not connect to device: " + device.getAddress());
		} 

		return bluetoothSocket;
	}
	
	public boolean getConnectionStatus() {
		return this.connectionStatus;
	}

	public BluetoothSocketWrapper getBluetoothSocket() {
		return bluetoothSocket;
	}

	public static interface BluetoothSocketWrapper {

		InputStream getInputStream() throws IOException;

		OutputStream getOutputStream() throws IOException;

		String getRemoteDeviceName();

		void connect() throws IOException;

		String getRemoteDeviceAddress();

		void close() throws IOException;

		BluetoothSocket getUnderlyingSocket();

	}

	public static class NativeBluetoothSocket implements BluetoothSocketWrapper {

		private BluetoothSocket socket;

		public NativeBluetoothSocket(BluetoothSocket tmp) {
			this.socket = tmp;
		}

		@Override
		public InputStream getInputStream() throws IOException {
			return socket.getInputStream();
		}

		@Override
		public OutputStream getOutputStream() throws IOException {
			return socket.getOutputStream();
		}

		@Override
		public String getRemoteDeviceName() {
			return socket.getRemoteDevice().getName();
		}

		@Override
		public void connect() throws IOException {
			socket.connect();
		}

		@Override
		public String getRemoteDeviceAddress() {
			return socket.getRemoteDevice().getAddress();
		}

		@Override
		public void close() throws IOException {
			socket.close();
		}

		@Override
		public BluetoothSocket getUnderlyingSocket() {
			return socket;
		}

	}

	public class FallbackBluetoothSocket extends NativeBluetoothSocket {

		private BluetoothSocket fallbackSocket;

		public FallbackBluetoothSocket(BluetoothSocket tmp) throws FallbackException {
			super(tmp);
			try {
				Class<?> clazz = tmp.getRemoteDevice().getClass();
				Class<?>[] paramTypes = new Class<?>[] { Integer.TYPE };
				Method m = clazz.getMethod("createRfcommSocket", paramTypes);
				Object[] params = new Object[] { Integer.valueOf(1) };
				fallbackSocket = (BluetoothSocket) m.invoke(tmp.getRemoteDevice(), params);
			} catch (Exception e) {
				throw new FallbackException(e);
			}
		}

		@Override
		public InputStream getInputStream() throws IOException {
			return fallbackSocket.getInputStream();
		}

		@Override
		public OutputStream getOutputStream() throws IOException {
			return fallbackSocket.getOutputStream();
		}

		@Override
		public void connect() throws IOException {
			fallbackSocket.connect();
		}

		@Override
		public void close() throws IOException {
			fallbackSocket.close();
		}

	}

	public static class FallbackException extends Exception {

		private static final long serialVersionUID = 1L;

		public FallbackException(Exception e) {
			super(e);
		}

	}
}